Skip to content

Fix UAF of a suspended coroutine referenced only via a closure - #1569

Merged
saghul merged 1 commit into
masterfrom
fix-uaf-gc-async-coro
Jul 27, 2026
Merged

Fix UAF of a suspended coroutine referenced only via a closure#1569
saghul merged 1 commit into
masterfrom
fix-uaf-gc-async-coro

Conversation

@saghul

@saghul saghul commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@saghul

saghul commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@bnoordhuis Here is the context for the GC-stress PR. When I added it to txiki.js one of my tests started to fail.

This is the smallest possible repro.

I have an AI generated fix but I'm not super-confident about its correctness here.

@bnoordhuis bnoordhuis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about this approach. Apart from all the other changes, enlarging JSVarRef is undesirable.

It's only possible when the async function's promise is not awaited, right? What about putting all live promises on a linked list and keep them artificially alive during GC?

Pro: also a useful tool for debugging promise leaks on context destruction.
Con: adds a O(n) step to the GC phase.

Another approach/hack: artificially increase the promise's reference count by one when it's created, then decrement it when it's awaited, then'd, etc. In other words, prevent it from getting reclaimed unless it's consumed by something else.

Comment thread tests/suspended-coroutine-closure-gc.js
@saghul

saghul commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look! Those sound like good ideas, I'll take a look!

@saghul
saghul force-pushed the fix-uaf-gc-async-coro branch from 6469041 to 46a3d0f Compare July 23, 2026 09:49
@saghul

saghul commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@bnoordhuis I did another pass (cleanup pending). The new approach doesn't extend the size of JSVarRef.

I couldn't get your proposal(s) to work because the bug also affects 2 more case without promises involved: plain generators and the mapped arguments fast path.

Comment thread quickjs.c
(e.g. mapped `arguments`) must stay non-coro even though cur_gc_obj is
later set. Fits existing padding, so JSVarRef does not grow. Cleared
when the var_ref is detached; the ref is released on detach/free. */
uint8_t is_coro;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this implied by !var_ref->detached && var_ref->stack_frame->cur_gc_obj != NULL? If that's the case, it'd be better to remove is_coro and have something like bool js_is_coro(JSVarRef *vr).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment, it can't because it changes over time.

Comment thread quickjs.c
@saghul
saghul marked this pull request as ready for review July 23, 2026 20:45
A suspended coroutine (async function, generator or async generator) keeps
its activation frame in a heap-allocated GC object. When a closure captures
one of the coroutine's locals it holds an *open* var_ref that points into
that frame. Open var_refs were not GC objects and were not traced by their
holders, so the edge closure -> var_ref -> coroutine frame was invisible to
the cycle collector: a coroutine reachable only through such a closure was
collected while still live, and resuming it (or running the closure) then
touched freed memory.

Make an open var_ref that captures a coroutine local a GC object holding a
counted reference to the owning coroutine (JSStackFrame.cur_gc_obj) and mark
it from every var_ref holder, so the collector sees the edge and keeps the
suspended coroutine (and the frame the var_ref points into) alive.
@saghul
saghul force-pushed the fix-uaf-gc-async-coro branch from 46a3d0f to c34d173 Compare July 27, 2026 10:32
@saghul saghul changed the title UAF of a suspended coroutine referenced only via a closure Fix UAF of a suspended coroutine referenced only via a closure Jul 27, 2026
@saghul
saghul merged commit 7955cfd into master Jul 27, 2026
128 checks passed
@saghul
saghul deleted the fix-uaf-gc-async-coro branch July 27, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants